﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections;
using System.ComponentModel;
using System.Collections.Specialized;

namespace SRTSolutions.Elevate
{
    /// <summary>
    /// A dictionary that fires events when items are added or removed (useful if you want to databind to a dictionary)
    /// </summary>
    /// <typeparam name="TKey">The type of the dictionary's keys</typeparam>
    /// <typeparam name="TValue">The type of the dictionary's values</typeparam>
    public class ObservableDictionary<TKey, TValue> :
        IDictionary<TKey, TValue>,
        ICollection<KeyValuePair<TKey, TValue>>,
        IEnumerable<KeyValuePair<TKey, TValue>>,
        IDictionary,
        ICollection,
        IEnumerable,
        INotifyPropertyChanged,
        INotifyCollectionChanged
    {
        private Dictionary<TKey, TValue> dictionary;

        /// <summary>
        /// Parameterless constructor
        /// </summary>
        public ObservableDictionary()
        {
            dictionary = new Dictionary<TKey, TValue>();
        }

        /// <summary>
        /// Constructor that takes an IDictionary
        /// </summary>
        /// <param name="dictionary">The base dictionary</param>
        public ObservableDictionary(Dictionary<TKey, TValue> dictionary)
        {
            this.dictionary = dictionary;
        }

        /// <summary>
        /// Add an item to the dictionary
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void Add(TKey key, TValue value)
        {
            var newKvp = new KeyValuePair<TKey, TValue>(key, value);
            dictionary.Add(key, value);
            OnNotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newKvp, -1));
        }

        /// <summary>
        /// Removes an item from the dictionary if it contains the provided key
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public bool Remove(TKey key)
        {
            bool result = false;
            if (dictionary.ContainsKey(key))
            {
                var removedKvp = new KeyValuePair<TKey, TValue>(key, dictionary[key]);
                result = dictionary.Remove(key);
                if (result)
                    OnNotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedKvp, -1));
            }
            return result;
        }

        /// <summary>
        /// See if the key is in the dictionary
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public bool ContainsKey(TKey key)
        {
            return dictionary.ContainsKey(key);
        }


        /// <summary>
        /// See if the item is in the dictionary and return it if it is
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public bool TryGetValue(TKey key, out TValue value)
        {
            return dictionary.TryGetValue(key, out value);
        }

        /// <summary>
        /// Get the key collection for the dictionary
        /// </summary>
        public ICollection<TKey> Keys
        {
            get { return dictionary.Keys; }
        }

        /// <summary>
        /// Get the value collection for the dictionary
        /// </summary>
        public ICollection<TValue> Values
        {
            get { return dictionary.Values; }
        }

        /// <summary>
        /// Get or set the value associated with the given key
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public TValue this[TKey key]
        {
            get
            {
                return dictionary[key];
            }
            set
            {
                var original = dictionary[key];
                dictionary[key] = value;
                OnNotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, new KeyValuePair<TKey, TValue>(key, value), new KeyValuePair<TKey, TValue>(key, original)));
            }
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly
        {
            get { return ((ICollection<KeyValuePair<TKey, TValue>>)dictionary).IsReadOnly; }
        }

        /// <summary>
        /// Add the key value pair to the dictionary
        /// </summary>
        /// <param name="item"></param>
        void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
        {
            ((ICollection<KeyValuePair<TKey, TValue>>)dictionary).Add(item);
            OnNotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, -1));
        }

        /// <summary>
        /// Clear all items from the dictionary
        /// </summary>
        public void Clear()
        {
            var kvps = dictionary.Select(kvp => kvp).ToList();
            dictionary.Clear();
            OnNotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, kvps));
        }

        /// <summary>
        /// See if the dictionary contains the given key value pair
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public bool Contains(KeyValuePair<TKey, TValue> item)
        {
            return dictionary.Contains(item);
        }

        /// <summary>
        /// Copy the dictionary values to an array
        /// </summary>
        /// <param name="array"></param>
        /// <param name="arrayIndex"></param>
        public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
        {
            dictionary.CopyTo(array, arrayIndex);
        }

        /// <summary>
        /// Get the number of items in the dictionary
        /// </summary>
        public int Count
        {
            get { return dictionary.Count; }
        }

        /// <summary>
        /// Remove the key value pair from the dictionary if it's in there
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
        {
            var result = ((ICollection<KeyValuePair<TKey, TValue>>)dictionary).Remove(item);
            if (result)
                OnNotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, -1));
            return result;
        }

        /// <summary>
        /// Get the typed enumerator for the dictionary
        /// </summary>
        /// <returns></returns>
        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
        {
            return ((IEnumerable<KeyValuePair<TKey, TValue>>)dictionary).GetEnumerator();
        }

        /// <summary>
        /// Get the enumerator for the dictionary
        /// </summary>
        /// <returns></returns>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return ((IEnumerable)dictionary).GetEnumerator();
        }

        /// <summary>
        /// Event that is fired if a property on the dictionary changes
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(PropertyChangedEventArgs args)
        {
            var eventToFire = PropertyChanged;
            if (eventToFire == null)
                return;
            eventToFire(this, args);
        }

        /// <summary>
        /// Event that is fired if the items in the dictionary change
        /// </summary>
        public event NotifyCollectionChangedEventHandler CollectionChanged;
        private void OnNotifyCollectionChanged(NotifyCollectionChangedEventArgs args)
        {
            var eventToFire = CollectionChanged;
            if (eventToFire == null)
                return;
            eventToFire(this, args);
        }

        /// <summary>
        /// Get the enumerator for the dictionary
        /// </summary>
        /// <returns></returns>
        IDictionaryEnumerator IDictionary.GetEnumerator()
        {
            return ((IDictionary)dictionary).GetEnumerator();
        }

        /// <summary>
        /// Get the keys collection for the dictionary
        /// </summary>
        ICollection IDictionary.Keys
        {
            get { return ((IDictionary)dictionary).Keys; }
        }

        /// <summary>
        /// Get the values collection for the dictionary
        /// </summary>
        ICollection IDictionary.Values
        {
            get { return ((IDictionary)dictionary).Values; }
        }

        /// <summary>
        /// Adds an element with the provided key and value to the IDictionary object.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        void IDictionary.Add(object key, object value)
        {
            ((IDictionary)dictionary).Add(key, value);
            OnNotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new KeyValuePair<object, object>(key, value)));
        }

        /// <summary>
        /// Removes the element with the specified key from the IDictionary object
        /// </summary>
        /// <param name="key"></param>
        void IDictionary.Remove(object key)
        {
            var idictionary = (IDictionary)dictionary;
            if (idictionary.Contains(key))
            {
                var removedKvp = new KeyValuePair<object, object>(key, idictionary[key]);
                idictionary.Remove(key);
                OnNotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedKvp, -1));
            }
        }

        /// <summary>
        /// Determines if the IDictionary object contains an element with the specified key
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        bool IDictionary.Contains(object key)
        {
            return ((IDictionary)dictionary).Contains(key);
        }

        /// <summary>
        /// Gets a value indicating if the IDictionary object is fixed size.
        /// </summary>
        bool IDictionary.IsFixedSize
        {
            get { return ((IDictionary)dictionary).IsFixedSize; }
        }

        /// <summary>
        /// Gets a value indicating if the IDictionary object is read-only
        /// </summary>
        bool IDictionary.IsReadOnly
        {
            get { return ((IDictionary)dictionary).IsReadOnly; }
        }

        /// <summary>
        /// Get or set the value associated with the given key
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        object IDictionary.this[object key]
        {
            get
            {
                return ((IDictionary)dictionary)[key];
            }
            set
            {
                var original = ((IDictionary)dictionary)[key];
                ((IDictionary)dictionary)[key] = value;
                OnNotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, new KeyValuePair<object, object>(key, value), new KeyValuePair<object, object>(key, original)));
            }
        }

        /// <summary>
        /// Copy the elements of the ICollection to the given Array, starting at the Array index
        /// </summary>
        /// <param name="array"></param>
        /// <param name="index"></param>
        void ICollection.CopyTo(Array array, int index)
        {
            ((ICollection)dictionary).CopyTo(array, index);
        }

        /// <summary>
        /// Gets a value indicating whether access to the ICollection is synchronized (thread safe)
        /// </summary>
        bool ICollection.IsSynchronized
        {
            get 
            {
                return ((ICollection)dictionary).IsSynchronized;
            }
        }

        /// <summary>
        /// Gets an object that can be used to synchronize access to the ICollection
        /// </summary>
        object ICollection.SyncRoot
        {
            get 
            {
                return ((ICollection)dictionary).SyncRoot;
            }
        }
    }
}
